#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
#include "utils.h"
#include "forecasting.h"
#include "operation_list.h"

#define REQ_FILE_PREFIX "initial.data/requests.server."
#define TRAFFIC_FILE_PREFIX "results/traffic.server."

int main(int argc, char** argv) {

  /* Distance between server */
  int server_distance[NUMBER_OF_SERVERS][NUMBER_OF_SERVERS];

  /* Servers where each content exists */
  server_list servers_for_content[NUMBER_OF_CONTENTS];

  /* Forecasting constants */
  double src = 10000.0, sic = 10000.0, smc = 10000.0;
  int HW = 1000;

  int period, content, server;

  /* Servers variables */
  int closest_replica[NUMBER_OF_CONTENTS];

  int indirect_services[NUMBER_OF_CONTENTS],
    local_services[NUMBER_OF_CONTENTS],
    maintenance[NUMBER_OF_CONTENTS];

  int server_requests[NUMBER_OF_SERVERS][NUMBER_OF_CONTENTS];

  MPI_Init(&argc, &argv);

  int my_rank;
  double total_traffic = 0.0;

  MPI_Comm_rank(MPI_COMM_WORLD, &my_rank);

  /* Initializing the distances between servers, origin servers *
   * and maintenance for contents */
  init_server_distance(server_distance);
  init_origin_server(servers_for_content);
  init_closest_replica(closest_replica, servers_for_content);

  /* Opening the request file for this server to read */
  char req_file_path[255];
  sprintf(req_file_path, "%s%d", REQ_FILE_PREFIX, my_rank);
  FILE *req_file = fopen(req_file_path, "r");

  if (req_file == NULL) {
    fprintf(stderr, "Can't open the server requests file: %s\n", req_file_path);
    MPI_Abort(MPI_COMM_WORLD, -1);
  }

  /* Opening the traffic file for this server to write */
  char traffic_file_path[255];
  sprintf(traffic_file_path, "%s%d", TRAFFIC_FILE_PREFIX, my_rank);
  FILE *traffic_file = fopen(traffic_file_path, "w");

  if (traffic_file == NULL) {
    fprintf(stderr, "Can't open the server requests file: %s\n", req_file_path);
    MPI_Abort(MPI_COMM_WORLD, -1);
  }

  operation_message op;

  operation_list operations;
  init_operation_list(&operations);

  for (period = 0; period < NUMBER_OF_PERIODS; period++) {
    double bic, brc, bmc;
    double ic, rc, mc;
    int load = 0;

    /* Parsing requests from request file */
    char line[1000];
    char *token;

    /* In every period, all contents must receive maintenance as the article says */
    for (content = 0; content < NUMBER_OF_CONTENTS; content++) {
      maintenance[content] = 1;
    }

    if (fgets(line, 1000, req_file) == NULL) {
      fprintf(stderr, "Incorrect number of periods in request file: %s for server: %d\n", req_file_path, my_rank);
      MPI_Abort(MPI_COMM_WORLD, -1);
    }

    content = 0;

    token = strtok(line, " ");

    while (token != NULL) {
      if (is_content_present(servers_for_content[content], my_rank)) {
        indirect_services[content] = 0;
        local_services[content] = atoi(token);
      } else {
        indirect_services[content] = atoi(token);
        local_services[content] = 0;
      }

      token = strtok(NULL, " ");
      content++;
    }

    if (content != NUMBER_OF_CONTENTS) {
      fprintf(stderr, "Incorrect number of requests in period %d for server %d\n", period, my_rank);
      MPI_Abort(MPI_COMM_WORLD, -1);
    }

    for (content = 0; content < NUMBER_OF_CONTENTS; content++) {
      int distance = server_distance[my_rank][closest_replica[content]];
      int origin_server = servers_for_content[content].head->server;
      int origin_distance = server_distance[my_rank][origin_server];

      /* Calculating the total traffic generated per content */
      ic = indirect_services[content] * sic * distance;
      mc = maintenance[content] * smc * origin_distance;
      rc = 0;

      int services = indirect_services[content] + local_services[content];
      double local_forecast = forecast_demand(services);

      /* Forecasting the total traffic due to indirect service, replication
         and maintenance */
      bic = local_forecast * sic * distance;
      brc = src * distance;

      double maintenance_forecast = forecast_demand(maintenance[content]);
      bmc = maintenance_forecast * smc * origin_distance;

      /* Evaluating the need of pulling a copy of the content */
      if (origin_server != my_rank) {
        if (!is_content_present(servers_for_content[content], my_rank) && (bic > (brc + bmc))) {
          op.operation = PULL;
          op.content = content;
          op.server = my_rank;

          rc = src * distance;

          insert_operation(&operations, op);
        } else if (is_content_present(servers_for_content[content], my_rank) && (bic < bmc)) {
          op.operation = DELETE;
          op.content = content;
          op.server = my_rank;

          insert_operation(&operations, op);
        }
      }

      /* Writing total traffic generated into traffic file for this server */
      fprintf(traffic_file, "%10.2lf ", (ic + mc + rc));
      total_traffic += (ic + mc + rc);
    }

    /* Broadcasting the indirect services for each server */
    for (server = 0; server < NUMBER_OF_SERVERS; server++) {
      for (content = 0; content < NUMBER_OF_CONTENTS; content++) {
        int tag = closest_replica[content];
        if (my_rank == server) {
          int i;
          for (i = 0; i < NUMBER_OF_SERVERS; i++)
            if (i != my_rank)
              MPI_Send(&indirect_services[content], 1, MPI_INT, i, tag, MPI_COMM_WORLD);
        } else {
          int indirect;
          MPI_Status status;
          MPI_Recv(&indirect, 1, MPI_INT, server, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
          if (status.MPI_TAG == my_rank)
            server_requests[status.MPI_SOURCE][content] = indirect;
          else
            server_requests[status.MPI_SOURCE][content] = 0;
        }
      }
    }

    fprintf(traffic_file, "\n");

    for (server = 0; server < NUMBER_OF_SERVERS; server++) {
      if (server != my_rank) {
        for (content = 0; content < NUMBER_OF_CONTENTS; content++) {
          double indirect_forecast = forecast_demand(server_requests[server][content]);
          load += indirect_forecast;
          server_requests[server][content] = indirect_forecast;
        }
      }
    }

    while (load > HW) {
      matrix_position max = get_max_load(server_requests);
      /* Recalculate load considering the push of a copy of the most required *
       * content for a server */
      load -= server_requests[max.line][max.column];
      server_requests[max.line][max.column] = 0;

      op.operation = PUSH;
      op.content = max.column;
      op.server = max.line;

      insert_operation(&operations, op);
    }

    /* Creating a MPI type to be used for broadcasting the *
     * operations between servers. */
    MPI_Datatype OP_MSG;
    operation_message *msg;
    build_operation_datatype(msg, &OP_MSG);

    /* Broadcasting operations */
    for (server = 0; server < NUMBER_OF_SERVERS; server++) {
      int number_of_operations;

      if (my_rank == server)
        number_of_operations = operations.size;

      MPI_Bcast(&number_of_operations, 1, MPI_INT, server, MPI_COMM_WORLD);

      if (my_rank == server) {
        while (operations.size > 0) {
          op = get_operation(operations);
          delete_operation(&operations);

          MPI_Bcast(&op, 1, OP_MSG, server, MPI_COMM_WORLD);

          /* Evaluating the closest replica */
          if ((op.operation == PULL) || (op.operation == PUSH)) {

            insert_server(&(servers_for_content[op.content]), op.server);

          } else if (op.operation == DELETE) {
            
            delete_server(&(servers_for_content[op.content]), op.server);

          }

          closest_replica[op.content] = get_closest_server(servers_for_content[op.content], server_distance[my_rank], server);

        }
      } else {
        int i;

        for (i = 0; i < number_of_operations; i++) {
          MPI_Bcast(&op, 1, OP_MSG, server, MPI_COMM_WORLD);
          
          /* Evaluating the closest replica */
          if ((op.operation == PULL) || (op.operation == PUSH)) {

            insert_server(&(servers_for_content[op.content]), op.server);

          } else if (op.operation == DELETE) {
            
            delete_server(&(servers_for_content[op.content]), op.server);

          }

          closest_replica[op.content] = get_closest_server(servers_for_content[op.content], server_distance[my_rank], my_rank);
          
        }
      }
    }
    printf("\nEnd of period %d for server %d\n", period, my_rank);

  }

  /* Closing requests and traffic files for this server */
  fprintf(traffic_file, "\n\nTotal traffic: %lf\n\n", total_traffic);

  fclose(req_file);
  fclose(traffic_file);

  MPI_Finalize();

  return 0;
}
